/*
 * OldXQueryDocument.java
 */

package xq.swing.text;

import java.awt.Color;
import java.awt.FontMetrics;
import javax.swing.JTextPane;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;

/**
 *
 * @author ThorntonRP
 */
class OldXQueryDocument extends DefaultStyledDocument {

	private static final long serialVersionUID = 7620518049323136825L;
	
	private static final Color NORMAL_COLOR   = Color.BLACK;
	private static final Color KEYWORD_COLOR  = new Color(  0,   0, 230);
	private static final Color VARIABLE_COLOR = new Color(  0, 153,   0);
	private static final Color COMMENT_COLOR  = new Color(150, 150, 150);
	private static final Color QUOTE_COLOR    = new Color(206, 123,   0);
	private static final Color TAG_COLOR      = new Color(115,   0, 115);

	private Element rootElement;

	private boolean multiLineComment;
	private boolean codeCompletionEnabled;
	
	private MutableAttributeSet normal;
	private MutableAttributeSet keyword;
	private MutableAttributeSet variable;
	private MutableAttributeSet comment;
	private MutableAttributeSet quote;
	private MutableAttributeSet tag;

	private final static String PRAGMA_START = "(::";
	private final static String PRAGMA_END   = "::)";
	
	private final static String COMMENT_START = "(:";
	private final static String COMMENT_END   = ":)";
	
	private final static String TAG_START = "<";
	private final static String TAG_END = ">";
	
	public OldXQueryDocument() {
		rootElement = getDefaultRootElement();
		putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n");

		normal = new SimpleAttributeSet();
		StyleConstants.setForeground(normal, NORMAL_COLOR);

		comment = new SimpleAttributeSet();
		StyleConstants.setForeground(comment, COMMENT_COLOR);
		StyleConstants.setItalic(comment, true);

		keyword = new SimpleAttributeSet();
		StyleConstants.setForeground(keyword, KEYWORD_COLOR);

		variable = new SimpleAttributeSet();
		StyleConstants.setForeground(variable, VARIABLE_COLOR);

		quote = new SimpleAttributeSet();
		StyleConstants.setForeground(quote, QUOTE_COLOR);
		
		tag = new SimpleAttributeSet();
		StyleConstants.setForeground(tag, TAG_COLOR);
	}

	/**
	 *  Override to apply syntax highlighting after the document has been updated
	 */
	@Override
	public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
		str = completeCode(str, offset);
		super.insertString(offset, str, a);
		processChangedLines(offset, str.length());
	}

	/**
	 *  Override to apply syntax highlighting after the document has been updated
	 */
	@Override
	public void remove(int offset, int length) throws BadLocationException {
		super.remove(offset, length);
		processChangedLines(offset, 0);
	}

	/**
	 *  Determine how many lines have been changed,
	 *  then apply highlighting to each line
	 */
	private void processChangedLines(int offset, int length) throws BadLocationException {
		String content = getText(0, getLength());

		//  The lines affected by the latest document update

		int startLine = rootElement.getElementIndex(offset);
		int endLine = rootElement.getElementIndex(offset + length);

		//  Make sure all comment lines prior to the start line are commented
		//  and determine if the start line is still in a multi line comment

		multiLineComment = commentLinesBefore(content, startLine);

		//  Do the actual highlighting

		for (int i = startLine; i <= endLine; i++) {
			applyHighlighting(content, i);
		}

		//  Resolve highlighting to the next end multi line delimiter

		if (multiLineComment) {
			commentLinesAfter(content, endLine);
		} else {
			highlightLinesAfter(content, endLine);
		}
	}

	/**
	 *  Highlight lines when a multi line comment is still 'open'
	 *  (ie. matching end delimiter has not yet been encountered)
	 */
	private boolean commentLinesBefore(String content, int line) {
		int offset = rootElement.getElement(line).getStartOffset();

		//  Start of comment not found, nothing to do

		int startDelimiter = lastIndexOf(content, COMMENT_START, offset - 2);

		if (startDelimiter < 0) {
			return false;		//  Matching start/end of comment found, nothing to do
		}
		int endDelimiter = indexOf(content, COMMENT_END, startDelimiter);

		if (endDelimiter < offset & endDelimiter != -1) {
			return false;		//  End of comment not found, highlight the lines
		}
		setCharacterAttributes(startDelimiter, offset - startDelimiter + 1, comment, false);
		return true;
	}

	/**
	 *  Highlight comment lines to matching end delimiter
	 */
	private void commentLinesAfter(String content, int line) {
		int offset = rootElement.getElement(line).getEndOffset();

		//  End of comment not found, nothing to do

		int endDelimiter = indexOf(content, COMMENT_END, offset);

		if (endDelimiter < 0) {
			return;		//  Matching start/end of comment found, comment the lines
		}
		int startDelimiter = lastIndexOf(content, COMMENT_START, endDelimiter);

		if (startDelimiter < 0 || startDelimiter <= offset) {
			setCharacterAttributes(offset, endDelimiter - offset + 1, comment, false);
		}
	}

	/**
	 *  Highlight lines to start or end delimiter
	 */
	private void highlightLinesAfter(String content, int line) throws BadLocationException {
		int offset = rootElement.getElement(line).getEndOffset();

		//  Start/End delimiter not found, nothing to do

		int startDelimiter = indexOf(content, COMMENT_START, offset);
		int endDelimiter = indexOf(content, COMMENT_END, offset);

		if (startDelimiter < 0) {
			startDelimiter = content.length();
		}
		if (endDelimiter < 0) {
			endDelimiter = content.length();
		}
		int delimiter = Math.min(startDelimiter, endDelimiter);

		if (delimiter < offset) {
			return;		//	Start/End delimiter found, reapply highlighting
		}
		int endLine = rootElement.getElementIndex(delimiter);

		for (int i = line + 1; i < endLine; i++) {
			Element branch = rootElement.getElement(i);
			Element leaf = getCharacterElement(branch.getStartOffset());
			AttributeSet as = leaf.getAttributes();

			if (as.isEqual(comment)) {
				applyHighlighting(content, i);
			}
		}
	}

	/**
	 *  Parse the line to determine the appropriate highlighting
	 */
	private void applyHighlighting(String content, int line) throws BadLocationException {
		int startOffset = rootElement.getElement(line).getStartOffset();
		int endOffset = rootElement.getElement(line).getEndOffset() - 1;

		int lineLength = endOffset - startOffset;
		int contentLength = content.length();

		if (endOffset >= contentLength) {
			endOffset = contentLength - 1;		//  check for multi line comments
			//  (always set the comment attribute for the entire line)
		}
		if (endingMultiLineComment(content, startOffset, endOffset) || multiLineComment || startingMultiLineComment(content, startOffset, endOffset)) {
			setCharacterAttributes(startOffset, endOffset - startOffset + 1, comment, false);
			return;
		}

		//  set normal attributes for the line

		setCharacterAttributes(startOffset, lineLength, normal, true);

		//  check for single line comment

//		int index = content.indexOf(getSingleLineCommentDelimiter(), startOffset);
		int index = -1;

		if ((index > -1) && (index < endOffset)) {
			setCharacterAttributes(index, endOffset - index + 1, comment, false);
			endOffset = index - 1;
		}

		//  check for tokens

		checkForTokens(content, startOffset, endOffset);
	}

	/**
	 *  Does this line contain the start delimiter
	 */
	private boolean startingMultiLineComment(String content, int startOffset, int endOffset) throws BadLocationException {
		int index = indexOf(content, COMMENT_START, startOffset);

		if ((index < 0) || (index > endOffset)) {
			return false;
		} else {
			multiLineComment = true;
			return true;
		}
	}

	/**
	 *  Does this line contain the end delimiter
	 */
	private boolean endingMultiLineComment(String content, int startOffset, int endOffset) throws BadLocationException {
		int index = indexOf(content, COMMENT_END, startOffset);

		if ((index < 0) || (index > endOffset)) {
			return false;
		} else {
			multiLineComment = false;
			return true;
		}
	}

	/**
	 *	Parse the line for tokens to highlight
	 */
	private void checkForTokens(String content, int startOffset, int endOffset) {
		while (startOffset <= endOffset) {
			
			//  skip the delimiters to find the start of a new token
			while (isTokenStartDelimiter(content.charAt(startOffset))) {
				if (startOffset < endOffset) {
					startOffset++;
				} else {
					return;
				}
			}

			//  Extract and process the entire token
			char c = content.charAt(startOffset);
			if (isXmlTagDelimiter(c)) {
				startOffset = getXmlTagToken(content, startOffset, endOffset);
			} else if (isQuoteDelimiter(c)) {
				startOffset = getQuoteToken(content, startOffset, endOffset);
			} else {
				startOffset = getOtherToken(content, startOffset, endOffset);
			}
		}
	}
	
	/**
	 * 
	 */
	private int getXmlTagToken(String content, int startOffset, int endOffset) {
		int endOfTag = content.indexOf(TAG_END, startOffset);
		if (endOfTag < 0) {
			endOfTag = endOffset;
		}
		setCharacterAttributes(startOffset, endOfTag - startOffset + 1, tag, false);
		return endOfTag + 1;
	}

	/**
	 *
	 */
	private int getQuoteToken(String content, int startOffset, int endOffset) {
		String quoteDelimiter = content.substring(startOffset, startOffset + 1);
		String escapeString = getEscapeString(quoteDelimiter);

		int index;
		int endOfQuote = startOffset;

		//  skip over the escape quotes in this quote

		index = content.indexOf(escapeString, endOfQuote + 1);

		while ((index > -1) && (index < endOffset)) {
			endOfQuote = index + 1;
			index = content.indexOf(escapeString, endOfQuote);
		}

		// now find the matching delimiter

		index = content.indexOf(quoteDelimiter, endOfQuote + 1);

		if ((index < 0) || (index > endOffset)) {
			endOfQuote = endOffset;
		} else {
			endOfQuote = index;
		}
		setCharacterAttributes(startOffset, endOfQuote - startOffset + 1, quote, false);

		return endOfQuote + 1;
	}

	/**
	 *
	 */
	private int getOtherToken(String content, int startOffset, int endOffset) {
		int endOfToken = startOffset + 1;

		while (endOfToken <= endOffset) {
			char c = content.charAt(endOfToken);
			if (isTokenEndDelimiter(c)) {
				if (c == '<') {
					return endOfToken;
				}
				break;
			}
			endOfToken++;
		}

		String token = content.substring(startOffset, endOfToken);

		if (isKeyword(token)) {
			setCharacterAttributes(startOffset, endOfToken - startOffset, keyword, false);
		} else if (isVariable(token)) {
			setCharacterAttributes(startOffset, endOfToken - startOffset, variable, false);
		}

		return endOfToken + 1;
	}

	/**
	 *  Assume the needle will the found at the start/end of the line
	 */
	private int indexOf(String content, String needle, int offset) {
		int index;

		while ((index = content.indexOf(needle, offset)) != -1) {
			String text = getLine(content, index).trim();

			if (text.startsWith(needle) || text.endsWith(needle)) {
				break;
			} else {
				offset = index + 1;
			}
		}

		return index;
	}

	/**
	 *  Assume the needle will the found at the start/end of the line
	 */
	private int lastIndexOf(String content, String needle, int offset) {
		int index;

		while ((index = content.lastIndexOf(needle, offset)) != -1) {
			String text = getLine(content, index).trim();

			if (text.startsWith(needle) || text.endsWith(needle)) {
				break;
			} else {
				offset = index - 1;
			}
		}

		return index;
	}

	/**
	 * 
	 */
	private String getLine(String content, int offset) {
		int line = rootElement.getElementIndex(offset);
		Element lineElement = rootElement.getElement(line);
		int start = lineElement.getStartOffset();
		int end = lineElement.getEndOffset();
		return content.substring(start, end - 1);
	}

	private boolean isTokenStartDelimiter(char c) {
//		String tokenDelimiters = ";:{}()[]+-/%<=>!&|^~*";
//		String tokenDelimiters = ";{}()[]+/%=!&|^~*";
		String tokenDelimiters = ";{}()[]+/%=!|^~*";
		if (Character.isWhitespace(c) || tokenDelimiters.indexOf(c) != -1) {
			return true;
		} else {
			return false;
		}
	}

	private boolean isTokenEndDelimiter(char c) {
//		String tokenDelimiters = ";:{}()[]+-/%<=>!&|^~*";
//		String tokenDelimiters = ";{}()[]+/%=!&|^~*";
		String tokenDelimiters = ";{}()[]+/%=!|^~*<";
		if (Character.isWhitespace(c) || tokenDelimiters.indexOf(c) != -1) {
			return true;
		} else {
			return false;
		}
	}

	private boolean isXmlTagDelimiter(char c) {
		String xmlTagDelimiters = TAG_START;
		if (xmlTagDelimiters.indexOf(c) < 0) {
			return false;
		} else {
			return true;
		}
	}
	
	private boolean isQuoteDelimiter(char c) {
		String quoteDelimiters = "\"'";
		if (quoteDelimiters.indexOf(c) < 0) {
			return false;
		} else {
			return true;
		}
	}

	/**
	 *  Override for other languages
	 */
	private boolean isKeyword(String token) {
		return OldXQuerySyntax.KEYWORDS.contains(token);
	}
	
	private boolean isVariable(String token) {
		return token.charAt(0) == '$';
	}

	/**
	 *  Override for other languages
	 */
	private String getEscapeString(String quoteDelimiter) {
		return "\\" + quoteDelimiter;
	}

	/**
	 *
	 */
	private String completeCode(String str, int offset) throws BadLocationException {
		String result = str;
		if (codeCompletionEnabled) {
			if (str.equals("{")) {
				StringBuffer whiteSpace = new StringBuffer();
				int line = rootElement.getElementIndex(offset);
				int i = rootElement.getElement(line).getStartOffset();
				while (true) {
					String temp = getText(i, 1);

					if (temp.equals(" ") || temp.equals("\t")) {
						whiteSpace.append(temp);
						i++;
					} else {
						break;
					}
				}
				result = "{\n" + whiteSpace.toString() + "\t\n" + whiteSpace.toString() + "}";
			}
		}
		return result;
	}

	/**
	 * 
	 */
	private static class SyntaxStyledEditorKit extends StyledEditorKit {

		private static final long serialVersionUID = -7348739062909779552L;

		@Override
		public Document createDefaultDocument() {
			return new OldXQueryDocument();
		}
	}

	/**
	 * 
	 */
	private static void setTabs(JTextPane textPane, int charactersPerTab) {
		FontMetrics fm = textPane.getFontMetrics(textPane.getFont());
		int charWidth = fm.charWidth('w');
		int tabWidth = charWidth * charactersPerTab;

		TabStop[] tabs = new TabStop[100];

		for (int j = 0; j < tabs.length; j++) {
			int tab = j + 1;
			tabs[j] = new TabStop(tab * tabWidth);
		}

		TabSet tabSet = new TabSet(tabs);
		SimpleAttributeSet attributes = new SimpleAttributeSet();
		StyleConstants.setTabSet(attributes, tabSet);
		int length = textPane.getDocument().getLength();
		textPane.getStyledDocument().setParagraphAttributes(0, length, attributes, false);
	}
/*
	public static void main(String a[]) {

		SyntaxStyledEditorKit editorKit = new SyntaxStyledEditorKit();

		final JTextPane editor = new JTextPane();
		editor.setFont(new Font("Monospaced", Font.PLAIN, 13));
		editor.setEditorKitForContentType("text/java", editorKit);
		editor.setContentType("text/java");
		setTabs(editor, 4);
		/*
		JButton button = new JButton("Load SyntaxDocument.java");
		button.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				try {
//					FileInputStream fis = new FileInputStream("SyntaxDocument.java");
					FileInputStream fis = new FileInputStream("C:\\Documents and Settings\\ThorntonRP\\My Documents\\NetBeansProjects\\xquery-xq\\src\\xq\\swing\\text\\SyntaxDocument.java");
					editor.read(fis, null);
					editor.requestFocusInWindow();
					setTabs(editor, 4);
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		});
		*//*
		JFrame frame = new JFrame("Syntax Highlighting");
		frame.setLayout(new BorderLayout());
		frame.add(new JScrollPane(editor), BorderLayout.CENTER);
//		frame.add(button, BorderLayout.SOUTH);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setSize(800, 300);
		frame.setVisible(true);
	}
*/
}
